/*******************************************************************************
 * Copyright (c) 2025-08-05 @author <a href="mailto:iffiff1@gmail.com">Tyler Chen</a>.
 * All rights reserved.
 *
 * Contributors:
 *     <a href="mailto:iffiff1@gmail.com">Tyler Chen</a> - initial API and implementation.
 ******************************************************************************/
package org.iff.util;

import lombok.Getter;
import org.apache.commons.lang3.StringUtils;

import javax.net.ssl.*;
import java.io.IOException;
import java.net.*;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.Flow;
import java.util.function.Consumer;

/**
 * HttpClientUtil
 *
 * @author <a href="mailto:iffiff1@gmail.com">Tyler Chen</a>
 * @since 2025-08-05
 */
public class HttpClientUtil {
    private static final String CHARSET = "UTF-8";

    public static String get(String uri, HttpOptions options) throws IOException {
        return doRequest("GET", uri, null, options);
    }

    public static void get(String uri, HttpOptions options, Consumer<byte[]> receive, Consumer<Throwable> receiveError) throws IOException {
        doRequest("GET", uri, null, options, receive, receiveError);
    }

    public static String post(String uri, String body, HttpOptions options) throws IOException {
        return doRequest("POST", uri, body, options);
    }

    public static void post(String uri, byte[] body, HttpOptions options, Consumer<byte[]> receive, Consumer<Throwable> receiveError) throws IOException {
        doRequest("POST", uri, body, options, receive, receiveError);
    }

    public static String delete(String uri, HttpOptions options) throws IOException {
        return doRequest("DELETE", uri, null, options);
    }

    public static void delete(String uri, HttpOptions options, Consumer<byte[]> receive, Consumer<Throwable> receiveError) throws IOException {
        doRequest("DELETE", uri, null, options, receive, receiveError);
    }

    public static String doRequest(String method, String uri, String body, HttpOptions options) throws IOException {
        options = Optional.ofNullable(options).orElse(HttpOptions.create());
        method = blankDefault(StringUtils.toRootUpperCase(method), "GET");
        HttpClient client = clientBuilder(options).build();
        Charset charset = Charset.forName(blankDefault(options.requestCharset, CHARSET));
        StringBuilder sb = new StringBuilder(128);
        BytesSubscriber subscriber = new BytesSubscriber(bs -> sb.append(new String(bs, charset)));
        HttpResponse.BodySubscriber<Void> voidBodySubscriber = HttpResponse.BodySubscribers.fromSubscriber(subscriber);
        HttpRequest request = null;
        if ("GET".equals(method)) {
            request = requestBuilder(uri, options).GET().build();
        } else if ("POST".equals(method)) {
            request = requestBuilder(uri, options).POST(HttpRequest.BodyPublishers.ofString(body, charset)).build();
        } else if ("PUT".equals(method)) {
            request = requestBuilder(uri, options).PUT(HttpRequest.BodyPublishers.ofString(body, charset)).build();
        } else if ("DELETE".equals(method)) {
            request = requestBuilder(uri, options).DELETE().build();
        }
        client.sendAsync(request, responseInfo -> voidBodySubscriber).join();
        return sb.toString();
    }

    public static void doRequest(String method, String uri, byte[] body, HttpOptions options, Consumer<byte[]> receive, Consumer<Throwable> receiveError) throws IOException {
        options = Optional.ofNullable(options).orElse(HttpOptions.create());
        method = blankDefault(StringUtils.toRootUpperCase(method), "GET");
        HttpClient client = clientBuilder(options).build();
        BytesSubscriber subscriber = new BytesSubscriber(receive, receiveError);
        HttpResponse.BodySubscriber<Void> voidBodySubscriber = HttpResponse.BodySubscribers.fromSubscriber(subscriber);
        HttpRequest request = null;
        if ("GET".equals(method)) {
            request = requestBuilder(uri, options).GET().build();
        } else if ("POST".equals(method)) {
            request = requestBuilder(uri, options).POST(HttpRequest.BodyPublishers.ofByteArray(body)).build();
        } else if ("PUT".equals(method)) {
            request = requestBuilder(uri, options).PUT(HttpRequest.BodyPublishers.ofByteArray(body)).build();
        } else if ("DELETE".equals(method)) {
            request = requestBuilder(uri, options).DELETE().build();
        }
        client.sendAsync(request, responseInfo -> voidBodySubscriber).join();
    }

    private static HttpClient.Builder clientBuilder(HttpOptions options) throws IOException {
        HttpClient.Builder clientBuilder = HttpClient.newBuilder()
                // 指定HTTP协议的版本
                .version(HttpClient.Version.HTTP_2)
                // 指定重定向策略
                .followRedirects(HttpClient.Redirect.NORMAL)
                // 指定超时的时长
                .connectTimeout(options.connectTimeout);// 如有必要，可通过该方法指定代理服务器地址
        SSLContext ctx;
        try {
            ctx = SSLContext.getInstance("TLS");
            ctx.init(new KeyManager[0], new TrustManager[]{new DefaultTrustManager()}, new SecureRandom());
            clientBuilder.sslContext(ctx);
        } catch (Exception e) {
            throw new IOException(e);
        }
        if (options.proxy != null) {
            clientBuilder.proxy(options.proxy);
        }
        return clientBuilder;
    }

    private static HttpRequest.Builder requestBuilder(String uri, HttpOptions options) throws IOException {
        HttpRequest.Builder builder = HttpRequest.newBuilder()
                .timeout(options.requestTimeout)
                .header("Accept-Charset", "UTF-8, ISO-8859-1");
        //.header("Accept", "text/xml,text/javascript,text/html,application/json,text/event-stream");
        for (Map.Entry<String, String> entry : options.headers.entrySet()) {
            String name = entry.getKey(), value = entry.getValue();
//            if (options.requestCharset != null && "Content-Type".equalsIgnoreCase(name) && !StringUtils.contains(value, "charset")) {
//                builder.header("Content-Type", value + "; charset=" + options.getRequestCharset().toLowerCase());
//                continue;
//            }
            builder.header(name, value);
        }
        builder.uri(URI.create(appendQueryParams(uri, options)));
        return builder;
    }

    private static String buildQuery(HttpOptions options) throws IOException {
        Map<String, String> params = options.params;
        if (params == null || params.isEmpty()) {
            return "";
        }
        StringBuilder query = new StringBuilder();
        Set<Map.Entry<String, String>> entries = params.entrySet();
        boolean hasParam = false;

        for (Map.Entry<String, String> entry : entries) {
            String name = entry.getKey();
            String value = entry.getValue();
            if (hasParam) {
                query.append("&");
            } else {
                hasParam = true;
            }
            query.append(name).append("=").append(URLEncoder.encode(value, blankDefault(options.requestCharset, CHARSET)));
        }
        return query.toString();
    }

    private static String appendQueryParams(String url, HttpOptions options) throws IOException {
        String query = buildQuery(options);
        if (query.isEmpty()) {
            return url;
        }
        return url + (url.contains("?") ? "&" : "?") + query;
    }

    private static String blankDefault(String value, String defaultValue) {
        return value != null && value.trim().length() > 0 ? value : defaultValue;
    }

    @Getter
    public static class HttpOptions {
        private Duration connectTimeout = Duration.ofSeconds(10);
        private Duration requestTimeout = Duration.ofMinutes(5);
        private Map<String, String> headers = new LinkedHashMap<>();
        private Map<String, String> params = new LinkedHashMap<>();
        private String requestCharset = CHARSET;
        private ProxySelector proxy;


        public static HttpOptions create() {
            return new HttpOptions();
        }

        public HttpOptions connectTimeout(int seconds) {
            connectTimeout = Duration.ofSeconds(seconds);
            return this;
        }

        public HttpOptions requestTimeout(int seconds) {
            requestTimeout = Duration.ofSeconds(seconds);
            return this;
        }

        public HttpOptions header(String name, String value) {
            headers.put(name, value);
            return this;
        }

        public HttpOptions param(String name, String value) {
            params.put(name, value);
            return this;
        }

        public HttpOptions requestCharset(String charset) {
            requestCharset = charset;
            return this;
        }

        public HttpOptions proxy(String host, int port) {
            proxy = ProxySelector.of(new InetSocketAddress(host, port));
            return this;
        }

        public HttpOptions authorization(String authorization) {
            headers.put("Authorization", authorization);
            return this;
        }

        public HttpOptions json() {
            headers.put("Content-Type", "application/json; charset=UTF-8");
            return this;
        }

        public HttpOptions text() {
            headers.put("Content-Type", "text/plain; charset=UTF-8");
            return this;
        }

        public HttpOptions removeHeader(String name) {
            headers.remove(name);
            return this;
        }

        public HttpOptions removeParam(String name) {
            params.remove(name);
            return this;
        }
    }

    public static class BytesSubscriber implements Flow.Subscriber<List<ByteBuffer>> {
        private final Consumer<byte[]> consumer;
        private final Consumer<Throwable> errorConsumer;
        private Flow.Subscription subscription;

        public BytesSubscriber(final Consumer<byte[]> consumer) {
            this.consumer = consumer;
            this.errorConsumer = (e) -> {
                throw new RuntimeException(e);
            };
        }

        public BytesSubscriber(final Consumer<byte[]> consumer, Consumer<Throwable> errorConsumer) {
            this.consumer = consumer;
            this.errorConsumer = errorConsumer != null ? errorConsumer : (e) -> {
                throw new RuntimeException(e);
            };
        }

        @Override
        public void onSubscribe(Flow.Subscription subscription) {
            this.subscription = subscription;
            subscription.request(1);
        }

        @Override
        public void onNext(List<ByteBuffer> item) {
            byte[] data = new byte[item.stream().mapToInt(ByteBuffer::remaining).sum()];
            int offset = 0;
            for (ByteBuffer buffer : item) {
                int remain = buffer.remaining();
                buffer.get(data, offset, remain);
                offset += remain;
            }
            if (data.length > 0) {
                consumer.accept(data);
            }
            subscription.request(1);
        }

        @Override
        public void onError(Throwable throwable) {
            errorConsumer.accept(throwable);
        }

        @Override
        public void onComplete() {

        }
    }

    private static class DefaultTrustManager implements X509TrustManager {
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }

        public void checkClientTrusted(X509Certificate[] chain, String authType) {
        }

        public void checkServerTrusted(X509Certificate[] chain, String authType) {
        }
    }
}
